Recover the stranded Translation Bridge work, and measure whether it is worth finishing - #84
Merged
Merged
Conversation
PR #19 landed only the first half of that branch: the shared-memory IPC itself. Five later commits never merged, and by the time they were reviewed their other consumer — Injekt — had been archived (#83). This port keeps the parts that survive that removal; nothing here references Injekt. Dictionary engine: - single hash-indexed storage instead of two parallel maps, so a duplicate key updates in place and the entry count stops drifting - 500k-entry cap to bound memory on oversized imports - CSV parser that honours quoted fields and "" escapes - batch lookup, mtime-based hot reload, save/load of a dictionary dir Shared memory: - Response Data becomes a real circular buffer with a C#-advanced tail; a full buffer now marks the slot Error instead of overwriting replies the plugin has not read - Shmem moves behind an Arc<SharedShmem> co-owned by the server thread, replacing the raw-pointer-as-usize handoff and the unsound Send/Sync on TranslationBridge - header counters are written once per batch from the Rust stats, which removes the read-modify-write race against the plugin Cache misses now feed an mpsc queue drained by translation_bridge_drain_misses for AI fallback. Its non-Windows stub already shipped with e3262e5; this adds the Windows implementation and the missing main.rs registration. The Tauri commands reach the dictionary and the miss queue directly rather than through Mutex<TranslationBridge>, dropping a lock level. Dictionary APIs whose only caller was Injekt are kept behind #[allow(dead_code)] for gs-hook to pick up. Verified: cargo check, cargo test translation_bridge (22 passed, two of them new), clippy, tsc --noEmit, eslint, i18n:check, tauri:check-cmds, dead:check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The recovered circular buffer adds response_data_tail to SharedMemoryHeader, which changes the layout every client must agree on, but the branch left PROTOCOL_VERSION at 1. Nothing breaks today — the only would-be client is GameStringer.Satellite, whose QueryBackend is still a TODO returning null — and that is exactly why the bump is free now and expensive later. is_valid() already rejects a version mismatch, so a stale plugin built against the v1 layout fails the handshake instead of misreading offsets. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The open question was whether a Named Pipe round trip holds up at rendering frequency, or whether the shared memory bridge is worth finishing. Measured instead of guessed: same workload on both, 3000 iterations after warmup, release build. Named Pipe p50 9.2us p95 18.9us p99 30.4us shared memory p50 0.4us p95 0.5us p99 0.8us So the pipe is ~20x slower per string — and it does not matter. GSTranslator::Translate() checks a process-local cache first and only calls IPC on a miss, and that cache is persisted across sessions. The ~88 strings/frame the pipe affords at p95 is not a rendering budget, it is a budget of strings never seen before, which decays to zero within seconds of play. Measuring also surfaced that no request/response path is complete on both sides: the overlay pipe is one-way by design, the DLL's translator pipe has no Rust server, ipc_bridge's server is a loop that sleeps, and the shared memory plugin's QueryBackend still returns null. The DLL even looks for a pipe name Rust never declares. Recorded in the log with the rest. ipc_bench.rs compiles only under cfg(test), so it stays reproducible without entering the binary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous entry said the DLL looks for a pipe name Rust never declares, and that the two would not meet even if the server existed. That was wrong. Reading the UTF-16 strings out of the prebuilt DLLs the repo ships settles it: gs-hook.dll (x64, x86) GameStringerOverlay + GameStringerTranslator unity_auto_translator.dll GameStringerUETranslator They are two separate channels with two separate clients. unity_injector.rs and the Unity DLL agree on GameStringerUETranslator and are fine as they are; gs-hook is the one with no server and no Rust constant at all. Renaming either into the other would disconnect a prebuilt binary — the name lives in a C++ header and the DLL would have to be rebuilt, not just the Rust constant edited. No code changes: there was no misnamed pipe to repair. The real defect is unchanged and already recorded — both channels are missing their server. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rouges78
added a commit
that referenced
this pull request
Aug 21, 2026
…op, runtime fallback (#85) The measurement in #84 said finish the pipe, not the shared memory. This closes the loop: a string the game draws is asked for over IPC, translated, learned, and served from the dictionary the next time it appears — then, if the files never budged at all, the whole run falls back to translating at runtime. Rust server (translator_pipe.rs) speaks the wire format the shipped DLLs already embed: message-mode pipe, 12-byte header, UTF-16LE payload. It answers from the Translation Bridge dictionary and queues misses into the same channel translation_bridge_drain_misses drains. On a miss it stays silent by design — the DLL owns the timeout, and answering with the original would poison its local cache permanently. C++ client: gs-hook's dllmain never called IPC::Initialize, so the client compiled into the shipped DLLs was dormant. Waking it exposed three defects, all found by injecting into the GDI testapp rather than by reading code: - Translate() blocked up to 2s per miss on the thread that draws. - The pipe handle lacked FILE_FLAG_OVERLAPPED. On a synchronous handle the kernel serializes I/O per file object, so with the receive thread parked in ReadFile the render thread's WriteFile queued behind it forever — and that read could only complete once the request it was blocking had arrived. The game froze on the first miss and the server logged a connection and zero requests. - The dedup set had no expiry, and since the server stays silent on a miss, a string stayed pending forever and was never asked about again. The chain learned and could not tell. Drain loop (lib/translation-bridge-drain.ts) drains misses, translates with the app's AI stack, and feeds the dictionary back — with a per-session cap, persistence across restarts, and the rule that an empty or unchanged translation is never stored. Orchestrator (lib/translation/runtime-fallback.ts) decides when to switch paths, next to patch-outcome.ts and for the same reason: it cannot be tested inside a 4300-line component. Structural blockers are reported before the contingent one, and a partial static success is left alone. Verified: 891 unit tests, cargo tests, clippy, tsc, eslint, i18n gates, tauri:check-cmds, dead:check. End to end on a real injected process: miss "The dragon roars from the mountain." learn "[IT] The dragon roars from the mountain." hit "The dragon roars from the mountain." -> "[IT] The dragon..." Traps recorded in docs/METODI-DI-TRADUZIONE.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five commits on
claude/bold-banachnever merged after PR #19 took the first half of that branch. By the time they were reviewed, Injekt — the consumer of much of that code — had been archived (#83). This recovers the parts that survive Injekt's removal, then measures the question that recovery raised: is the shared memory bridge worth finishing at all?What came back
Nothing here references Injekt; the recoverable slice turned out to be self-contained.
Dictionary engine — a single hash-indexed store replaces two parallel maps, so a duplicate key updates in place and the entry count stops drifting. Plus a 500k-entry cap, a CSV parser that honours quoted fields and
""escapes, batch lookup, mtime-based hot reload, and save/load of a dictionary directory.Shared memory — Response Data becomes a real circular buffer with a tail the client advances. A full buffer now marks the slot
Errorinstead of overwriting replies the plugin has not read yet. TheShmemmoves behind anArc<SharedShmem>co-owned by the server thread, replacing the raw-pointer-as-usizehandoff and the unsoundSend/SynconTranslationBridge. Header counters are written once per batch from the Rust stats, removing the read-modify-write race against the plugin.AI fallback — cache misses feed an mpsc queue drained by
translation_bridge_drain_misses. Its non-Windows stub already shipped withe3262e58; main had an orphan command, defined only for Linux and never registered. This adds the Windows implementation and the missingmain.rsline.PROTOCOL_VERSIONgoes to 2: the newresponse_data_tailfield changes the layout every client must agree on, and the branch left the constant at 1.The measurement
Recovering this raised a fair question — is a Named Pipe fast enough, or does the shared memory path need finishing? Measured rather than guessed. Same workload on both, 3000 iterations after warmup, release build, reproducible via
cargo test --release --lib ipc_bench:The pipe is ~20x slower per string, and it does not matter.
GSTranslator::Translate()checks a process-local cache first and only calls IPC on a miss, and that cache is persisted across sessions. The ~88 strings/frame the pipe affords at p95 is not a rendering budget — it is a budget of strings never seen before, which decays to zero within seconds of play.ipc_bench.rscompiles only undercfg(test), so it stays reproducible without entering the binary.What the measurement turned up
No request/response path is complete on both sides. The overlay pipe is one-way by design;
GameStringerTranslatorhas a real client in gs-hook but no Rust server;ue_translator/ipc_bridge.rshas a server that is a loop callingsleep; and the shared memory plugin'sQueryBackendstill returnsnull.One correction is folded in: an earlier draft of the log claimed the DLL looks for a pipe name Rust never declares. Reading the UTF-16 strings out of the prebuilt DLLs shows otherwise —
gs-hook.dllcarriesGameStringerTranslator,unity_auto_translator.dllcarriesGameStringerUETranslator. Two separate channels with two separate clients, each correctly named. Renaming either into the other would disconnect a prebuilt binary. The real defect is the missing server, not the name.All of it is in
docs/METODI-DI-TRADUZIONE.mdwith the commands to re-check.Verification
cargo check(8 warnings, all pre-existing inprocess_utils.rs— the 3 intranslation_bridgeare gone),cargo test translation_bridge(22 passed, two of them new),cargo clippy(no new warnings),tsc --noEmit,eslint,i18n:check(802 = baseline),tauri:check-cmds(no invoke into the void),dead:check(no new dead modules).Worth knowing before merging
This is maintenance on a dormant subsystem, not a feature. The bugs it fixes are real and sitting in
maintoday, but nothing consumes the Translation Bridge yet. Given the numbers above, finishing the pipe server looks like the better investment than finishing the C#QueryBackend— the hard half of the pipe is already written inhook-dll/src/ipc.cpp.🤖 Generated with Claude Code